// Copyright (c) 2023 Presto Labs Pte. Ltd.
// Author: tianqiang

#include "coin2/exchange/mexc/api_order_v3/native_private_client.h"

#include <sstream>

#include <rapidjson/writer.h>

namespace coin2::exchange::mexc::api_order_v3 {
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;

#define CHECK_API_LIMIT(action_type, ip_weight, uid_weight)                               \
  auto ts = GetCurrentTimestamp();                                                        \
  if (!HasEnoughRateLimit(ts, action_type)) {                                             \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }                                                                                       \
  local_rate_tracker_->Push(ts, ip_weight, uid_weight);                                   \
  api_req_info_[__func__]++;

MexcNativePrivateClient::MexcNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config) {
  mea_ = mea;
  NativeClientConfig cfg;
  cfg.set_rest_host("api.mexc.com");
  cfg.set_rest_host_port(443);
  cfg.MergeFrom(config);
  LOG(INFO) << "[MexcNativePrivateClient] config: " << cfg.DebugString();

  auths_.Construct<MexcRestAuth, NativeClientConfig>(cfg);

  rate_tracker_ = std::make_unique<MexcRateTracker>(mea_, config);
  local_rate_tracker_ = dynamic_cast<MexcRateTracker*>(rate_tracker_.get());
  start_time_ = GetCurrentTimestamp();
}

auto MexcNativePrivateClient::QueryAccountBalance() -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::QUERY, 10, 0)

  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByGetWithSignature("/api/v3/account", &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto MexcNativePrivateClient::QueryOpenOrders(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::QUERY, 3, 0)

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  auto context = GetAuth()->CreateRequestByGetWithSignature("/api/v3/openOrders", &builder);

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto MexcNativePrivateClient::QueryAllOrders(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::QUERY, 10, 0)

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  auto context = GetAuth()->CreateRequestByGetWithSignature("/api/v3/allOrders", &builder);

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto MexcNativePrivateClient::QueryFillOrders(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::QUERY, 10, 0)

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  auto context = GetAuth()->CreateRequestByGetWithSignature("/api/v3/myTrades", &builder);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

// std::shared_ptr<AsioHttpContext> MexcNativePrivateClient::QueryOrdersByIds(
//     const RestParams& params) {
//   CHECK_API_LIMIT(ActionType::QUERY, 2, 0)

//   UrlParamsBuilder builder;
//   builder.PutGet("symbol", params.symbol());
//   builder.PutGet("origClientOrderId", params.client_order_id());
//   builder.PutGet("orderId", params.order_id());
//   auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v3/order", &builder);

//   context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
//   context->extra_info = DebugString();
//   return std::shared_ptr<AsioHttpContext>(context);
// }

void MexcNativePrivateClient::Prepare() {
  mb_->Subscribe("mexc/enable_time", [this](const std::string&, void* data) {
    int64_t value = reinterpret_cast<int64_t>(data);
    local_rate_tracker_->UpdateEnableTime(value);
    LOG(WARNING) << "Mexc native client enable_time: " << value;
  });
  mb_->Subscribe("mexc/listen_key", [this](const std::string& channel, void* data) {
    listen_key_ = *static_cast<std::string*>(data);

    LOG(INFO) << "MexcNativePrivateClient set listen key: " << channel << ", " << listen_key_;
  });
}

std::string MexcNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;
  std::stringstream ss;
  ss << "MexcNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

auto MexcNativePrivateClient::QueryDeposit(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::QUERY, 1, 0)

  UrlParamsBuilder builder;
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("startTime", params.start_time());
    builder.PutGet("endTime", params.end_time());
  }
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v3/capital/deposit/hisrec", &builder);

  context->query_type = MexcOrderSubTopicId::REST_DEPOSIT;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

auto MexcNativePrivateClient::QueryWithdrawal(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::QUERY, 1, 0)

  UrlParamsBuilder builder;
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("startTime", params.start_time());
    builder.PutGet("endTime", params.end_time());
  }
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v3/capital/withdraw/history", &builder);
  context->query_type = MexcOrderSubTopicId::REST_WITHDRAWAL;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

auto MexcNativePrivateClient::QueryTransfer(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::QUERY, 1, 0)

  if (!params.has_from_account_type() || !params.has_to_account_type()) {
    return {};
  }

  UrlParamsBuilder builder;
  std::string account_info;
  builder.PutGet("fromAccountType", params.from_account_type());
  builder.PutGet("toAccountType", params.to_account_type());

  account_info = params.from_account_type() + params.to_account_type();
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("startTime", params.start_time());
    builder.PutGet("endTime", params.end_time());
  }

  auto* context = GetAuth()->CreateRequestByGetWithSignature(
      "/api/v3/capital/sub-account/universalTransfer",
      &builder);

  context->query_type = MexcOrderSubTopicId::DEPOSIT_SUBACCOUNT;
  context->extra_info = account_info;
  return std::shared_ptr<AsioHttpContext>(context);
}

auto MexcNativePrivateClient::PlaceSingleOrder(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::PLACE, 1, 1)

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("side", params.trade_type());
  builder.PutGet("type", params.order_type());
  builder.PutGet("quantity", params.quantity());
  builder.PutGet("price", params.price());
  builder.PutGet("newClientOrderId", params.client_order_id());
  auto context = GetAuth()->CreateRequestByPostWithSignature("/api/v3/order", &builder);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto MexcNativePrivateClient::PlaceBatchOrders(const std::vector<RestParams>& params)
    -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::PLACE, 1, 1)

  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    writer.StartObject();

    writer.Key("symbol");
    writer.String(param.symbol().c_str());
    writer.Key("side");
    writer.String(param.trade_type().c_str());
    writer.Key("type");
    writer.String(param.order_type().c_str());
    writer.Key("quantity");
    writer.String(param.quantity().c_str());
    writer.Key("price");
    writer.String(param.price().c_str());
    writer.Key("newClientOrderId");
    writer.String(param.client_order_id().c_str());

    writer.EndObject();
  }

  writer.EndArray();

  UrlParamsBuilder builder;
  builder.PutGet("batchOrders", ApiSignatureUtil::EscapeURL(str_buf.GetString()));
  auto context = GetAuth()->CreateRequestByPostWithSignature("/api/v3/batchOrders", &builder);

  context->query_type = OrderSubTopicId::MULTI_SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto MexcNativePrivateClient::CancelOrder(const RestParams& params) -> HttpContextPtr {
  CHECK_API_LIMIT(ActionType::CANCEL, 1, 0)

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("orderId", params.order_id());
  builder.PutGet("origClientOrderId", params.client_order_id());
  auto context = GetAuth()->CreateRequestByDeleteWithSignature("/api/v3/order", &builder);

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto MexcNativePrivateClient::PlaceOrderNorm(const OrderExecution& order_exe) -> HttpContextPtr {
  if (!order_exe.is_place_batch_orders) {
    const auto& order_spec = *order_exe.order_spec;
    const auto proc_order_id = order_exe.proc_order_id;
    auto params = GetRestOrderParams(order_spec, proc_order_id);
    return PlaceSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->place_pending_order_deque.empty()) {
    return {};
  }

  OrderExecution::OrderSpecList temp_place_pending_order_deque;
  std::vector<RestParams> params;
  const auto& symbol = mutable_order_exe->place_pending_order_deque.front()->product;
  int cnt = 0;
  while (!mutable_order_exe->place_pending_order_deque.empty()) {
    const auto* order_spec = mutable_order_exe->place_pending_order_deque.front();
    mutable_order_exe->place_pending_order_deque.pop_front();

    if (order_spec->product != symbol || cnt >= 20) {
      temp_place_pending_order_deque.emplace_back(order_spec);
      continue;
    }

    mutable_order_exe->place_proceed_order_deque.emplace_back(order_spec);
    ++cnt;

    params.emplace_back(GetRestOrderParams(*order_spec, order_spec->proc_order_id));
  }

  mutable_order_exe->place_pending_order_deque = std::move(temp_place_pending_order_deque);

  return PlaceBatchOrders(params);
}

auto MexcNativePrivateClient::CancelOrderNorm(const OrderExecution& order_exe) -> HttpContextPtr {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

auto MexcNativePrivateClient::GetRestOrderParams(const OrderSpec& order_spec, int64_t proc_order_id)
    -> RestParams {
  RestParams params;
  params.set_symbol(order_spec.product);
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_trade_type("BUY");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_trade_type("SELL");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }
  if (order_spec.order_type != OrderType::LIMIT_ORDER) {
    NOTREACHED() << "unsupported order type: " << static_cast<int>(order_spec.order_type);
  }

  if (order_spec.post_only) {
    params.set_order_type("LIMIT_MAKER");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_order_type("IMMEDIATE_OR_CANCEL");
  } else if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_order_type("FILL_OR_KILL");
  } else {
    params.set_order_type("LIMIT");
  }

  auto product = MexcProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_quantity(formatter.FormatQty(order_spec.qty));
  params.set_price(formatter.FormatPrice(order_spec.price));

  srand(time(NULL));
  std::string client_order_id =
      std::to_string(std::rand() % 100000) + "_" + std::to_string(proc_order_id);
  params.set_client_order_id(client_order_id);

  return params;
}

auto MexcNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  RestParams params;

  params.set_symbol(order_context.native_product());

  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    params.set_client_order_id(std::to_string(order_context.proc_order_id()));
  }

  return params;
}

auto MexcNativePrivateClient::GenListenKey(bool is_init) -> HttpContextPtr {
  if (is_init && !listen_key_.empty()) {
    return nullptr;
  }

  UrlParamsBuilder builder;
  auto context = GetAuth()->CreateRequestByPostWithSignature("/api/v3/userDataStream", &builder);

  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

auto MexcNativePrivateClient::ExtendListenKey() -> HttpContextPtr {
  CHECK(!listen_key_.empty());

  UrlParamsBuilder builder;
  builder.PutGet("listenKey", listen_key_);
  auto context = GetAuth()->CreateRequestByPutWithSignature("/api/v3/userDataStream", &builder);

  context->query_type = OrderSubTopicId::LISTEN_KEY;
  context->extra_info = DebugString();
  return HttpContextPtr(context);
}

AsioHttpContextList MexcNativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  RestParams params;
  AsioHttpContextList list;
  auto fill_func = [&params](int64_t start_time, int64_t end_time) {
    params.set_start_time(start_time / 1000'000);
    params.set_end_time(end_time / 1000'000);
  };

  switch (query.type()) {
    case RestQueryProto::QUERY_TRANSFERS: {
      fill_func(query.start_time(), query.end_time());
      const static std::vector<std::string> account_type = {"SPOT", "FUTURES"};
      for (size_t i = 0; i < account_type.size(); i++) {
        for (size_t j = 0; j < account_type.size(); j++) {
          params.set_from_account_type(account_type[i]);
          params.set_to_account_type(account_type[j]);
          auto http_context = QueryTransfer(params);
          list.emplace_back(http_context);
        }
      }
      break;
    }
    case RestQueryProto::QUERY_WITHDRAW: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryWithdrawal(params));
      break;
    }

    case RestQueryProto::QUERY_DEPOSIT: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryDeposit(params));
      break;
    }

    default: {
      return list;
    }
  }

  return list;
}

}  // namespace coin2::exchange::mexc::api_order_v3
